<!DOCTYPE html>
<html lang="zh-CN">
<!--浏览器搞笑标题-->
<script type="text/javascript" src="/js/FunnyTitle.js"></script>






<head>
  <meta charset="UTF-8">
  <link rel="apple-touch-icon" sizes="76x76" href="/binblog/img/binblog.ico">
  <link rel="icon" type="image/png" href="/binblog/img/binblog.ico">
  <meta name="viewport"
        content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  
  <meta name="theme-color" content="#2f4154">
  <meta name="description" content="">
  <meta name="author" content="leo-bin">
  <meta name="keywords" content="">
  <title>ZooKeeper深入浅出 - 阿斌的个人博客</title>

  <link  rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.4.1/css/bootstrap.min.css" />


  <link  rel="stylesheet" href="https://cdn.staticfile.org/github-markdown-css/4.0.0/github-markdown.min.css" />
  <link  rel="stylesheet" href="/binblog/lib/hint/hint.min.css" />

  
    <link  rel="stylesheet" href="https://cdn.staticfile.org/highlight.js/10.0.0/styles/dracula.min.css" />
  

  


<!-- 主题依赖的图标库，不要自行修改 -->

<link rel="stylesheet" href="//at.alicdn.com/t/font_1749284_yg9cfy8wd6.css">



<link rel="stylesheet" href="//at.alicdn.com/t/font_1736178_pjno9b9zyxs.css">


<link  rel="stylesheet" href="/binblog/css/main.css" />

<!-- 自定义样式保持在最底部 -->


<meta name="generator" content="Hexo 4.2.1"></head>


<body>
  <header style="height: 70vh;">
    <nav id="navbar" class="navbar fixed-top  navbar-expand-lg navbar-dark scrolling-navbar">
  <div class="container">
    <a class="navbar-brand"
       href="/binblog/">&nbsp;<strong>Abin的Blog</strong>&nbsp;</a>

    <button id="navbar-toggler-btn" class="navbar-toggler" type="button" data-toggle="collapse"
            data-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <div class="animated-icon"><span></span><span></span><span></span></div>
    </button>

    <!-- Collapsible content -->
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav ml-auto text-center">
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/binblog/">
                <i class="iconfont icon-home-fill"></i>
                首页
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/binblog/archives/">
                <i class="iconfont icon-archive-fill"></i>
                归档
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/binblog/categories/">
                <i class="iconfont icon-category-fill"></i>
                分类
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/binblog/tags/">
                <i class="iconfont icon-tags-fill"></i>
                标签
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/binblog/about/">
                <i class="iconfont icon-user-fill"></i>
                关于
              </a>
            </li>
          
        
        
          <li class="nav-item" id="search-btn">
            <a class="nav-link" data-toggle="modal" data-target="#modalSearch">&nbsp;&nbsp;<i
                class="iconfont icon-search"></i>&nbsp;&nbsp;</a>
          </li>
        
      </ul>
    </div>
  </div>
</nav>

    <div class="view intro-2" id="background" parallax=true
         style="background: url('/binblog/img/6.webp') no-repeat center center;
           background-size: cover;">
      <div class="full-bg-img">
        <div class="mask flex-center" style="background-color: rgba(0, 0, 0, 0.3)">
          <div class="container text-center white-text fadeInUp">
            <span class="h2" id="subtitle">
              
            </span>

            
              
  <div class="mt-3 post-meta">
    <i class="iconfont icon-date-fill" aria-hidden="true"></i>
    <time datetime="2020-08-09 01:43">
      2020年8月9日 凌晨
    </time>
  </div>


<div class="mt-1">
  
    
    <span class="post-meta mr-2">
      <i class="iconfont icon-chart"></i>
      6.5k 字
    </span>
  

  
    
    <span class="post-meta mr-2">
      <i class="iconfont icon-clock-fill"></i>
      
      
      70
       分钟
    </span>
  

  
  
    
      <!-- LeanCloud 统计文章PV -->
      <span id="leancloud-post-views-container" class="post-meta" style="display: none">
        <i class="iconfont icon-eye" aria-hidden="true"></i>
        <span id="leancloud-post-views"></span> 次
      </span>
    
  
</div>

            
          </div>

          
        </div>
      </div>
    </div>
  </header>

  <main>
    
      

<div class="container-fluid">
  <div class="row">
    <div class="d-none d-lg-block col-lg-2"></div>
    <div class="col-lg-8 nopadding-md">
      <div class="container nopadding-md" id="board-ctn">
        <div class="py-5" id="board">
          <div class="post-content mx-auto" id="post">
            
              <p class="note note-info">
                
                  本文最后更新于：2020年10月7日 晚上
                
              </p>
            
            <article class="markdown-body">
              <h1>ZooKeeper深入浅出</h1>
<h2 id="前言🔔"><a class="header-anchor" href="#前言🔔"></a>前言🔔</h2>
<p>今天来学习下分布式场景下必不可少的一个中间件服务<strong>ZooKeeper</strong>。</p>
<p><strong>老样子，照样给出官方地址：</strong></p>
<div class="hljs"><pre><code class="hljs xml">https://zookeeper.apache.org/</code></pre></div>
<h2 id="基本介绍✨"><a class="header-anchor" href="#基本介绍✨"></a>基本介绍✨</h2>
<h3 id="1）什么是ZooKeeper？"><a class="header-anchor" href="#1）什么是ZooKeeper？"></a>1）什么是ZooKeeper？</h3>
<p>听起来就像一个动物园一样，这些开源框架真有意思，起的名字都和动物有关。</p>
<p>什么<code>Hadoop</code>（大象），<code>Hive</code>（马蜂），<code>Tomcat</code>，<code>Ant</code>等等。</p>
<p><strong>言归正传。</strong></p>
<p><strong>我们首先看看官方对它的定义：</strong></p>
<div class="hljs"><pre><code class="hljs tex">ZooKeeper: A Distributed Coordination Service for Distributed Applications
zk：一种为分布式应用提供坐标的服务。</code></pre></div>
<br>
<p>ZooKeeper（以下都用<strong>ZK</strong>来代替）说白了就是一个<strong>远程数据库</strong>！</p>
<p>存什么的呢？数据库当然什么都可以存！但是ZK不一样。</p>
<p>ZK的内部数据结构是一个文件<strong>Znode</strong>，而文件的大小也有限制，不能超过<strong>1MB</strong>。</p>
<p>所以ZK只能存一些很关键，很重要的信息，比如说节点的配置信息，或者服务器的地址等等。</p>
<br>
<h3 id="2）为什么ZK这么火？"><a class="header-anchor" href="#2）为什么ZK这么火？"></a>2）为什么ZK这么火？</h3>
<p>你想呀，远程数据库是什么？当然用来存数据的！</p>
<p>如果可以，你甚至可以使用Map自己写一个本地数据库，然后对外开放端口，给别人用。</p>
<p>但是肯定没人用。因为<strong>不可靠，性能不好</strong>。</p>
<br>
<p><strong>1.首先我们说性能：</strong></p>
<p>既然是数据库服务，那肯定是要求查询速度和插入速度要快呀，要不然谁用？</p>
<p>现在市面上大多数的缓存中间件都是基于内存的，所以速度这方面自然是不用说的。</p>
<p>接下来就需要选定存储数据的<strong>数据结构</strong>了。</p>
<p>然后你想到了用<strong>Hash表</strong>，这个查起来贼快，插入也快的一批。</p>
<p>但是我们要知道，我们的数据之间是有关系的，你把数据放哈希表，不都乱成一锅粥了？</p>
<p>其次我们想到了<strong>二叉树</strong>，这个查起来性能也挺快的，插入删除的性能也不错。</p>
<p>而且，每一个数据作为一个节点，都有根节点和孩子节点，那么数据之间的关系就一目了然了。</p>
<p>（<strong><code>之后你会发现，ZK内部其实是哈希表+树的组合</code></strong>）</p>
<br>
<p><strong>2.现在我们来聊可靠性：</strong></p>
<p>你数据存进去，有一天要用了，但是<strong>数据突然丢失了</strong>怎么办？</p>
<p>搞不好，连你服务器都没了！这个时候你又怎么办？</p>
<br>
<p>可靠性其实可以分为<strong>三个方面</strong>去设计。</p>
<p><strong>1.事前</strong></p>
<p><strong>2.事中</strong></p>
<p><strong>3.事后</strong></p>
<p><strong>1.首先是事前，也就是服务器宕机之前。</strong></p>
<p>怎么做？</p>
<p>这个时候我们可以想到使用<strong>集群部署</strong>，搞个分布式，主从啥的。一个不行，我弄多个呗！</p>
<p>如果主节点挂了，从节点可以顶上去，继续提供服务。</p>
<p>那么又出现了一个问题，从节点顶上去，那起码从节点中的数据要和旧主节点中的基本一致吧。</p>
<p>要不然出现数据不一样，可靠性不就没了？</p>
<p>ok，那就需要考虑主从节点之间的通信和数据备份的问题。</p>
<p>主节点可以随时和从节点进行数据同步，但是主节点需要继续提供服务。</p>
<p>不可能真正做到实时的同步，所以数据难免会有偏差。</p>
<p>那么如何做到低延迟，又能够保持高可靠的数据一致性呢？</p>
<br>
<p><strong>2.事中，也就是，如果发生了大面积的请求，如何防止服务器挂掉？</strong></p>
<p>很简单，高并发请求来了，我们认怂呗！我们不去硬刚，想办法限流，挡一挡！</p>
<p>只要服务器还在，就可以继续为线上提供服务。有总比没有好吧。</p>
<p>具体怎么做呢？可以使用一些开源的限流组件。</p>
<p>比如说消息队列，或者Nginx做负载均衡。</p>
<p>当高并发请求过来的时候你用消息队列去档，消息队列呢只是接受请求请求，不用急着去处理，效率还是很高的。</p>
<p>大不了，你搞个消息队列的集群。</p>
<p>Nginx的话适用于你的后台是集群式部署的，在网络请求的时候就将大面积的密集请求都分散给服务器集群。</p>
<p>这样子也行，但是效率肯定是没有消息队列好。</p>
<p>所以这里建议使用消息队列的集群+Nginx做负载均衡。</p>
<p>首先将请求先走Nginx，Nginx将请求分散一部分，然后打给消息队列集群。</p>
<p>消息队列集群收到消息进行消息的积压，之后慢慢的消费。</p>
<p><strong>3.事后怎么做？事后就是服务器挂了之后你能干点啥？</strong></p>
<p>服务器挂了还能干嘛？赶紧重启呀！重启之后赶紧进行数据恢复呀，争取降低损失就行。</p>
<p>恢复？你拿什么恢复？当然是数据的备份呀！</p>
<p>ok，拿着最后一点就是如何进行数据的持久化！毕竟事实数据都是基于内存的。</p>
<p>一旦集器断电重启啥的，内存肯定是没了，只有去磁盘中找了。</p>
<p>所以如何在服务的同时进行数据的持久化，也是一个比较重要的地方。</p>
<p>一般的做法就是设置一个缓存区buffer。</p>
<p>这个buffer专门用来存储数据的更新操作日志。</p>
<p>每隔一段时间或者没执行完一次数据更新操作就进行一次刷盘，将buffer刷回磁盘，实现持久化。</p>
<br>
<p>所以在这里可以得出我们的结论：</p>
<p><strong>一个好的分布式协调服务最需要关注的就是集群之间的同步问题。</strong></p>
<p>ZooKeeper就是做到了上面所说的一切的辣个男人！（不愧是管理员！）</p>
<br>
<p>当然咯，ZK能够做到的肯定不止这些，我这里只是讲了比较关键的地方。</p>
<p>ZooKeeper 可以说是一个典型的分布式数据一致性解决方案。</p>
<p>分布式应用程序可以基于 ZooKeeper 实现诸如：</p>
<p><strong>1.数据发布/订阅</strong></p>
<p><strong>2.负载均衡</strong></p>
<p><strong>3.命名服务</strong></p>
<p><strong>4.分布式协调/通知</strong></p>
<p><strong>5.Master 选举</strong></p>
<p><strong>6.分布式锁和分布式队列</strong>等功能。</p>
<h2 id="基本命令🎵"><a class="header-anchor" href="#基本命令🎵"></a>基本命令🎵</h2>
<p><strong>1.创建节点并写数据：</strong></p>
<div class="hljs"><pre><code class="hljs shell">create /mypath mydata</code></pre></div>
 <br>
<p><strong>2.获取节点的信息：</strong></p>
<div class="hljs"><pre><code class="hljs shell">get /mypath</code></pre></div>
 <br>
<p><strong>3.修改节点中的数据：</strong></p>
<div class="hljs"><pre><code class="hljs shell">set /mypath mydata1</code></pre></div>
 <br>
<p><strong>4.删除节点：(前提是此节没有孩子节点了)</strong></p>
<div class="hljs"><pre><code class="hljs shell">delete /mypath</code></pre></div>
 <br>
<p><strong>5.删除节点（包括孩子节点一同删除）:</strong></p>
<div class="hljs"><pre><code class="hljs shell">rmr /mypath</code></pre></div>
 <br>
<p><strong>6.退出客户端终端shell</strong></p>
<div class="hljs"><pre><code class="hljs shell">quit</code></pre></div>
 <br>
<p><strong>7.查看某个路径下所有节点(/表示根目录)</strong></p>
<div class="hljs"><pre><code class="hljs shell">ls /</code></pre></div>
<br>
<p><strong><code>还有很多，可以参考官方给出的指令和解释</code></strong></p>
<h2 id="数据结构☀️"><a class="header-anchor" href="#数据结构☀️"></a>数据结构☀️</h2>
<h3 id="1）导入源码"><a class="header-anchor" href="#1）导入源码"></a>1）导入源码</h3>
<p>首先ZK是<strong>Java</strong>写的。</p>
<p>大家在下载zk的安装包的时候，里面默认就有ZK的源码。</p>
<p><img src="https://bins-pic.oss-cn-shanghai.aliyuncs.com/mypic/image-20200802164644367.png" srcset="/binblog/img/loading.gif" alt="image-20200802164644367"></p>
<br>
<p>当然源码是可以直接看到的，但是不完整，需要自己进行编译。</p>
<p>这里参考网上的教程就可以了，不多说了。<a href="https://blog.csdn.net/fujianfafu/article/details/80307240?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param&amp;depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param" target="_blank" rel="noopener"><code>地址</code></a></p>
<p><strong>编译成功之后直接使用IDEA导入项目就可以看到完整的源码了</strong></p>
<p>导入的时候记得选择<strong>eclipse</strong>的方式进行导入：</p>
<p><img src="https://bins-pic.oss-cn-shanghai.aliyuncs.com/mypic/image-20200804161344440.png" srcset="/binblog/img/loading.gif" alt="image-20200804161344440"></p>
<br>
<h3 id="2）介绍"><a class="header-anchor" href="#2）介绍"></a>2）介绍</h3>
<p>ZK中的数据就是一个节点<strong>Znode</strong>或者叫做目录组成的。</p>
<p>所有的节点在一起构成了一棵树。</p>
<p><strong>有点像我们的文件系统：</strong></p>
<p><img src="https://bins-pic.oss-cn-shanghai.aliyuncs.com/mypic/zk%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.jpg" srcset="/binblog/img/loading.gif" alt=""></p>
<br>
<p><strong>我们来看看ZNode的源码实现</strong></p>
<p>ZK中将ZNode封装成为一个叫做：<strong>DataNode</strong>的对象：</p>
<div class="hljs"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DataNode</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Record</span> </span>&#123;
    <span class="hljs-comment">/**父亲节点*/</span>
    DataNode parent;

    <span class="hljs-comment">/**此节点中的数据*/</span>
    <span class="hljs-keyword">byte</span> data[];

    <span class="hljs-comment">/**</span>
<span class="hljs-comment">     * acl指的是节点的权限对象的id值，DataTree中会维护一个acl列表，key就是这里的acl</span>
<span class="hljs-comment">     */</span>
    Long acl;

    <span class="hljs-comment">/**</span>
<span class="hljs-comment">     * stat表示节点的元数据，这部分是会持久化到磁盘的，主要有：节点的大小，版本号，事务id，时间戳等</span>
<span class="hljs-comment">     */</span>
    <span class="hljs-keyword">public</span> StatPersisted stat;

    <span class="hljs-comment">/**</span>
<span class="hljs-comment">     * 该节点的孩子节点</span>
<span class="hljs-comment">     */</span>
    <span class="hljs-keyword">private</span> Set&lt;String&gt; children = <span class="hljs-keyword">null</span>;
    
    <span class="hljs-comment">//省略其他代码和成员变量。。。</span>
&#125;</code></pre></div>
<br>
<p>这里还需简单介绍下关于<strong>ACL</strong>和<strong>Stat</strong></p>
<p><strong>ACL： Access Control List  访问控制列表</strong></p>
<div class="hljs"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ACL</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Record</span> </span>&#123;
  <span class="hljs-comment">//代表权限值：对节点的操作权限：读，写，删等</span>
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> perms;
  <span class="hljs-comment">//验证实体：</span>
  <span class="hljs-keyword">private</span> org.apache.zookeeper.data.Id id;
&#125;</code></pre></div>
<p><strong>ID：验证实体</strong></p>
<div class="hljs"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Id</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Record</span> </span>&#123;
  <span class="hljs-comment">//验证模式</span>
  <span class="hljs-keyword">private</span> String scheme;
  <span class="hljs-comment">//具体的id值</span>
  <span class="hljs-keyword">private</span> String id;
&#125;</code></pre></div>
<br>
<p><strong>Stat： 节点的持久化信息，这部分数据需要进行持久化的</strong></p>
<div class="hljs"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StatPersisted</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Record</span> </span>&#123;
  <span class="hljs-comment">//事务id</span>
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">long</span> czxid;
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">long</span> mzxid;
  <span class="hljs-comment">//节点的时间戳</span>
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">long</span> ctime;
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">long</span> mtime;
  <span class="hljs-comment">//三种版本号</span>
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> version;
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> cversion;
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> aversion;
  <span class="hljs-comment">//临时节点拥有者</span>
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">long</span> ephemeralOwner;
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">long</span> pzxid;
&#125;</code></pre></div>
<br>
<p>然后使用一个叫做<strong>DataTree</strong>的类来存储所有的节点：</p>
<div class="hljs"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DataTree</span> </span>&#123;
    <span class="hljs-comment">/**</span>
<span class="hljs-comment">     * 使用ConcurrentHashMap来存储所有的节点</span>
<span class="hljs-comment">     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> ConcurrentHashMap&lt;String, DataNode&gt; nodes =
        <span class="hljs-keyword">new</span> ConcurrentHashMap&lt;String, DataNode&gt;();

    <span class="hljs-comment">//管理所有节点的监听对象</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> WatchManager dataWatches = <span class="hljs-keyword">new</span> WatchManager();
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> WatchManager childWatches = <span class="hljs-keyword">new</span> WatchManager();

    <span class="hljs-comment">/**树的根目录，也就是根节点*/</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String rootZookeeper = <span class="hljs-string">"/"</span>;

    <span class="hljs-comment">/**</span>
<span class="hljs-comment">     * 使用另外一个Map来单独存储临时节点</span>
<span class="hljs-comment">     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> Map&lt;Long, HashSet&lt;String&gt;&gt; ephemerals =
        <span class="hljs-keyword">new</span> ConcurrentHashMap&lt;Long, HashSet&lt;String&gt;&gt;();
    
    <span class="hljs-comment">//省略其他代码。。。</span>
&#125;</code></pre></div>
<p>当然，不是说树吗？<strong>树的结构呢</strong>？别急！</p>
<p>你仔细看看上面的说的，我们使用Map已经实现了一颗树的构建过程</p>
<p>每一个节点都记录了它的父节点和孩子节点的引用。</p>
<p>我们一般都是通过节点的路径来找到某个节点，拿到节点了整个树的结构不就出来了？</p>
<h2 id="ZK中的集群模式🔊"><a class="header-anchor" href="#ZK中的集群模式🔊"></a>ZK中的集群模式🔊</h2>
<p><strong>ZK的集群宏观图：</strong></p>
<img src="https://bins-pic.oss-cn-shanghai.aliyuncs.com/mypic/ZK的宏观结构图.jpg" srcset="/binblog/img/loading.gif" style="zoom:150%;" />
<br>
<p>为了防止<strong>单点故障</strong>。ZK肯定也是支持<strong>集群模式</strong>的。</p>
<p>ZK本身的集群模式是：<strong>一主多从</strong></p>
<br>
<p>ZK中集群的节点一共有<strong>三种角色</strong>：</p>
<p><strong>1.Leader领导者</strong></p>
<p><strong>2.Follower跟随者</strong></p>
<p><strong>3.Observer观察者</strong></p>
<p><img src="https://bins-pic.oss-cn-shanghai.aliyuncs.com/mypic/%E4%B8%BB%E4%BB%8E%E9%9B%86%E7%BE%A4%E6%A8%A1%E5%BC%8F%E4%B8%8B%E7%9A%84%E6%9E%B6%E6%9E%84%E5%9B%BE.jpg" srcset="/binblog/img/loading.gif" alt=""></p>
<p>每一个ZK节点都可以接受客户端的请求。</p>
<p>所有节点都可以负责<strong>读</strong>请求。</p>
<p>但是只能由领导者负责<strong>写</strong>请求。</p>
<p>如果观察者和跟随者收到了写请求，它们会将写请求<strong>转发</strong>给领导者。</p>
<br>
<p><strong>三种角色存在的意义总结:</strong></p>
<img src="https://bins-pic.oss-cn-shanghai.aliyuncs.com/mypic/zk中节点的三种角色.jpg" srcset="/binblog/img/loading.gif" style="zoom:150%;" />
<p>观察者存在的意义就是为了提高<strong>读</strong>的性能。</p>
<p>因为在选举的过程中领导者和跟随着需要进行投票，无法立即对客户端的请求进行响应。</p>
<p>但是<strong>观察者</strong>是不参与投票过程的。它只会同步leader的状态。</p>
<h2 id="ZK是如何保证一致性的🔨"><a class="header-anchor" href="#ZK是如何保证一致性的🔨"></a>ZK是如何保证一致性的🔨</h2>
<p>ZK是通过一个叫做<strong>ZAB</strong>协议来保证数据一致性的。</p>
<p><strong>ZAB</strong>，<strong>ZooKeeper Atomic Broadcast</strong>   也叫做：<strong>ZK原子广播协议</strong></p>
<p>ZAB协议规定了每一个<strong>ZK节点</strong>都可能有<strong>三种状态：</strong></p>
<p><strong>1.Looking选举状态</strong></p>
<p><strong>2.Following跟随状态</strong></p>
<p><strong>3.Leading主节点的状态</strong></p>
<br>
<p>我们先要了解下<strong>zxid</strong>的概念：</p>
<p><strong>zxid：Zookeeper Transaction Id  ZK中的事务id</strong></p>
<p><strong>ZXID的结构：</strong></p>
<p><img src="https://bins-pic.oss-cn-shanghai.aliyuncs.com/mypic/ZXID%E7%BB%93%E6%9E%84.jpg" srcset="/binblog/img/loading.gif" alt=""></p>
<p>可以看到包括两个部分：</p>
<p>一个是<strong>Epoch纪元</strong>，一个是<strong>Counter</strong>计数。</p>
<p>纪元代表当前leader产生时的<strong>时间标识</strong>。但是这里用时间来理解不是很好。</p>
<p>我们可以把它当作一个id值，反正随着leader的变更纪元会越来大。</p>
<p>下一任leader的纪元肯定要大于上一任。</p>
<p>而Counter则是用来<strong>计数的</strong>。</p>
<p>某个服务器节点一旦接受到数据更新操作的请求，Counter就会+1，代表事务进度已经更新。</p>
<p><strong>纪元只会随着领导者的变化而变大</strong>。</p>
<br>
<p>ZK会给每一个来自客户端的请求分配一个<strong>全局唯一递增</strong>的zxid（<strong>counter</strong>来计数）。</p>
<p>这个编号反应了所有来自客户端的<strong>请求顺序</strong>的先后。</p>
<p>所以每一个集群节点都会保留一个全局的最大<strong>ZXID</strong></p>
<br>
<p>如果此时主节点挂掉了就会触发集群的<strong>崩溃恢复</strong>。</p>
<p>这一过程分为<strong>三个阶段</strong>：</p>
<h3 id="1）ZAB选举阶段"><a class="header-anchor" href="#1）ZAB选举阶段"></a>1）ZAB选举阶段</h3>
<p>在ZK的不同版本中有不同的选举算法的实现：</p>
<ul>
<li><strong>1 基于UDP的LeaderElection</strong></li>
<li><strong>2 基于UDP的FastLeaderElection</strong></li>
<li><strong>3 基于UDP和认证的FastLeaderElection</strong></li>
<li><strong>4 基于TCP的FastLeaderElection</strong></li>
</ul>
<p>但是前面三个算法在最新版的ZK中也就是3.4.X中都被废弃了。</p>
<p>默认使用的就是<strong>算法 4</strong>，<strong>快速Leader选举算法</strong>。</p>
<p><strong>FastLeaderElection：</strong></p>
<p><strong>ZXID</strong>:之前说过了，服务器的事务ID</p>
<p><strong>服务器ID</strong>：也叫做<strong>MyId</strong>，代表了服务器在集群中的编号ID</p>
<p><strong>选票的数据结构：</strong></p>
<p>我们知道，投票，投的真正是啥？在ZK中使用一种叫做<strong>选票</strong>的数据结构对票进行了封装</p>
<p><strong>具体封装的内容就是：</strong></p>
<p><strong>1.logicClock： 代表这是服务器发起的第多少轮投票</strong></p>
<p><strong>2.state：      服务器的目前状态</strong></p>
<p><strong>3.self_id:      服务器自己的编号id</strong></p>
<p><strong>4.self_zxid:   服务器中保存的最大zxid</strong></p>
<p><strong>5.vote_id:      被推荐的服务器id</strong></p>
<p><strong>6.vote_zxid:  被推荐的服务器的最大zxid</strong></p>
<p><strong>自增选举轮次：</strong></p>
<p>服务器每次发起一轮新的投票，选票内部的logicClock都会自增+1，更新投票轮次</p>
<p><strong>初始化选票阶段：</strong></p>
<p>在刚刚开始选举的时候，所有的服务器都会将票投给自己。</p>
<p><strong>判断选举轮次：</strong></p>
<p>服务器在收到一张选票之前会首先比较选票中的logicClock的值，有三种情况：</p>
<p>1.选票中的logicClock的值比自己维护的logicClock要大</p>
<p>这就说明自己的选票落后了，那就会主动将自己的logicClock值同步一下。</p>
<p>然后根据选票中的zxid来决定是否更新自己的选票中的zxid。</p>
<p>2.选票中的logicClock等于自己的</p>
<p>这个说明投票很正常，可以开始比较zxid了，进行选票PK</p>
<p>3.选票中的logicClock要小于自己的</p>
<p>这种就说明对方的选票过时了，直接抛弃就是。</p>
<p><strong>PK过程：</strong></p>
<p>如果logicClock一致，那就可以开始比较选票，进行PK了。</p>
<p>大致的PK过程就是比较选票中的<strong>vote_zxid</strong></p>
<p>如果对方的zxid比自己的要大，那就将票投给对方。</p>
<p>等于的话，就<strong>比较myid</strong>，也就是服务器id，<strong>优先选大的</strong>。</p>
<p><strong>统计选票：</strong></p>
<p>如果在某个时间点，集群中有过半的服务器认可了自己的选票（过半都投给了自己）</p>
<p>那么这个服务器就会变成<strong>准Leader</strong>。</p>
<br>
<p>为什么说是准Leader呢？当时是因为还没结束呀！</p>
<p>此时进入第二阶段：<strong>发现阶段</strong>。</p>
<h3 id="2）ZAB发现阶段"><a class="header-anchor" href="#2）ZAB发现阶段"></a>2）ZAB发现阶段</h3>
<p>这个时候准Leader会再次去剩下的从节点中找寻是否有最大的<strong>ZXID</strong>值存在。</p>
<p>这是因为之前在初次选举的过程中可能因为某些原因，比如说网络等。</p>
<p>可能会发生选举出多个准Leader。而且此时的ZXID并不是最大的，也就说数据不是最新的。</p>
<p>所以在这里进行第二次发现。接受来自剩下所有节点的Epoch。</p>
<p>从中找到 一个最新的Epoch值，然后+1发送给所有的从节点。</p>
<p>从节点收到纪元的值并更新自己ZXID中的纪元的值，代表<strong>新王登基</strong>！改朝换代！<strong>纪元</strong>随之改变。</p>
<p>并发送各自最大的ZXID和历史日志给leader。</p>
<p>Leader收到之后选出最大的ZXID，同步自己的数据。</p>
<p><strong>（这一块写的有点懵，没有看明白自己在写什么。需要再想想。）</strong></p>
<h3 id="3）ZAB数据同步阶段"><a class="header-anchor" href="#3）ZAB数据同步阶段"></a>3）ZAB数据同步阶段</h3>
<p>这个时候的准leader会将上一阶段收集到的最新历史数据同步发送给所有从节点。</p>
<p>只有超过半数以上的从节点同步成功之后，此时的准Leader正式成为<strong>Leader</strong>！</p>
<h3 id="4）集群刚刚启动的时候Leader是如何选出来的？"><a class="header-anchor" href="#4）集群刚刚启动的时候Leader是如何选出来的？"></a>4）集群刚刚启动的时候Leader是如何选出来的？</h3>
<p>这里只要记住，在集群刚刚开始启动的时候，所有服务器的初始zxid都是0</p>
<p>假设现在有三台服务器：A，B，C</p>
<p>它们的编号myid分别是：1，2，3</p>
<p>zxid都是0</p>
<p>根据我们之前讲过的选举流程</p>
<p>首先所有服务器都将票投给自己：</p>
<p><img src="https://bins-pic.oss-cn-shanghai.aliyuncs.com/mypic/ZAB%E9%9B%86%E7%BE%A4%E5%88%9A%E5%88%9A%E5%90%AF%E5%8A%A8%E6%97%B6%E7%9A%84%E9%80%89%E4%B8%BE1.jpg" srcset="/binblog/img/loading.gif" alt=""></p>
<p>现在所有服务器内部的票箱分别是： <strong>A[1，1]    B[2，2]    C[3，3]</strong></p>
<p>现在开始广播自己的选票给对方</p>
<p>对方收到时候，会发现所有的选票的logicClock都是一样的，那就开始比较zxid</p>
<p>但是此时的zxid也都是一样的，没办法那就只能比较myid</p>
<p>一个很显然的问题就是，服务器C的选票一定是最多的！毫无争议！</p>
<p><img src="https://bins-pic.oss-cn-shanghai.aliyuncs.com/mypic/ZAB%E9%9B%86%E7%BE%A4%E5%88%9A%E5%88%9A%E5%90%AF%E5%8A%A8%E6%97%B6%E7%9A%84%E9%80%89%E4%B8%BE2.jpg" srcset="/binblog/img/loading.gif" alt=""></p>
<p>可以看到服务器1和服务器2最后都是将票投给了服务器3</p>
<p>目前三个服务器内部的票箱情况是;</p>
<p>A[（1，3），（2，3），（3，3）]</p>
<p>B[（1，3），（2，3），（3，3）]</p>
<p>C[（1，3），（2，3），（3，3）]</p>
<p>ok，<strong>现在服务器3就成为了Leader了</strong></p>
<p>开始<strong>更新</strong>各个服务器之间的状态：</p>
<p><img src="https://bins-pic.oss-cn-shanghai.aliyuncs.com/mypic/ZAB%E9%9B%86%E7%BE%A4%E5%88%9A%E5%88%9A%E5%90%AF%E5%8A%A8%E6%97%B6%E7%9A%84%E9%80%89%E4%B8%BE3.jpg" srcset="/binblog/img/loading.gif" alt=""></p>
<h2 id="ZK如何实现主从之间数据同步？"><a class="header-anchor" href="#ZK如何实现主从之间数据同步？"></a>ZK如何实现主从之间数据同步？</h2>
<p>之前说过了，只有Leader才能处理写请求。</p>
<p>但是当Leader处理完写请求之后，剩下的从节点如何和主节点进行同步呢？</p>
<p>难道是单独的一个接一个去通知，然后进行同步？</p>
<p>不对，这样子性能是一个问题，而且效率太低了。</p>
<p>那就是<strong>广播</strong>的形式了咯，我一次将数据全部发送给所有从节点。</p>
<p>这样子好像效率挺高的，但是如果有些<strong>从节点同步失败</strong>呢？</p>
<p>是不是就会产生部分节点的数据不一致了？</p>
<p>这里就使用到了ZAB中的<strong>两阶段提交思想</strong>。也叫做<strong>2PC</strong></p>
<p>其核心思想就是主节点通过广播的形式将新数据发送给所有的从节点。</p>
<p>从节点收到消息之后进行同步，但是只是将数据写日志，并没有真的写（<strong>提交</strong>）。</p>
<p>然后写入成功之后发送一个ACK消息给主节点。</p>
<p>只有当主节点收到半数以上的ACK消息之后，才会继续广播一个正式提交的消息给从节点进行<strong>Commit</strong>。</p>
<br>
<p><strong>就像这样：</strong></p>
<p><img src="https://bins-pic.oss-cn-shanghai.aliyuncs.com/mypic/ZK%E4%B8%AD%E7%9A%842PC%E8%BF%87%E7%A8%8B.jpg" srcset="/binblog/img/loading.gif" alt=""></p>
<br>
<p>2PC是<strong>分布式事务</strong>的常用解决方案。</p>
<p>当然，2PC也并不是没有缺点。</p>
<p>就拿上面这个来说，如果有一个从节点确实也收到了Commit请求。</p>
<p>但是因为某些原因，自己在提交的时候还是失败了，所以还是会出现数据不一致的问题。</p>
<p>这样的话，我们其实还可以在提交失败的时候记录下请求，然后主节点定时的发送提交请求。</p>
<p>保证数据一定能够被同步成功。</p>
<h2 id="ZK如何保证并发请求下线程安全？"><a class="header-anchor" href="#ZK如何保证并发请求下线程安全？"></a>ZK如何保证并发请求下线程安全？</h2>
<p>对于大部分缓存中间件来说，它们的应用场景都是分布式的，集群的，高并发的。</p>
<p>通俗点来说，你就是一个为大家提供公用服务的服务员。同一时间可能会有多个请求的到来。</p>
<p>所以大部分的缓存中间件需要考虑的就是如何保证并发请求的线程安全问题。</p>
<p>对于我们熟知的Redis来说，它是基于单线程来处理所有的网络请求。</p>
<p>通过<strong>IO多路复用机制</strong>实现单线程也能够非常高效率的处理高并发的请求（<strong>10w qps</strong>）。</p>
<br>
<p><strong>那么ZK是如何做的呢？</strong></p>
<p>根据官方提供的资料来看，ZK内部是基于<strong>NIO模型</strong>来实现网络通信的。</p>
<p>而ZK内部的NIO的实现有两种，一种是基于原生JDK提供的NIO，一种是基于Netty3实现的NIO。</p>
<p>默认用的是<strong>原生NIO</strong>。</p>
<p>这里可以通过看源码中的<strong>依赖</strong>来验证：</p>
<p><img src="https://bins-pic.oss-cn-shanghai.aliyuncs.com/mypic/image-20200804163625287.png" srcset="/binblog/img/loading.gif" alt="image-20200804163625287"></p>
<br>
<p>具体的网络通信过程就不多说了。</p>
<p>这里直接聊聊ZK是如何处理一个网络的请求的。</p>
<p>在ZK中，默认是通过：<strong>FollowerRequestProcessor</strong>来处理所有的写请求</p>
<div class="hljs"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FollowerRequestProcessor</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ZooKeeperCriticalThread</span> <span class="hljs-keyword">implements</span></span>
<span class="hljs-class">        <span class="hljs-title">RequestProcessor</span> </span>&#123;
    
    <span class="hljs-comment">//zk服务，处理所有请求</span>
    FollowerZooKeeperServer zks;
    <span class="hljs-comment">//请求队列，使用阻塞队列来存储</span>
    LinkedBlockingQueue&lt;Request&gt; queuedRequests = <span class="hljs-keyword">new</span> LinkedBlockingQueue&lt;Request&gt;();
    <span class="hljs-keyword">boolean</span> finished = <span class="hljs-keyword">false</span>;

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span><span class="hljs-params">()</span> </span>&#123;
            <span class="hljs-keyword">while</span> (!finished) &#123;
                Request request = queuedRequests.take();
                <span class="hljs-comment">//查看请求是哪一种类型的：增，删，改等等</span>
                <span class="hljs-keyword">switch</span> (request.type) &#123;
                <span class="hljs-keyword">case</span> OpCode.create:
                <span class="hljs-keyword">case</span> OpCode.delete:
                <span class="hljs-keyword">case</span> OpCode.setData:
                <span class="hljs-keyword">case</span> OpCode.setACL:
                <span class="hljs-keyword">case</span> OpCode.createSession:
                <span class="hljs-keyword">case</span> OpCode.closeSession:
                <span class="hljs-keyword">case</span> OpCode.multi:
                    <span class="hljs-comment">//这些请求都交给zk服务进行转发</span>
                    zks.getFollower().request(request);
                    <span class="hljs-keyword">break</span>;
       &#125;
    &#125;
   &#125;

    <span class="hljs-comment">//处理每一个到来的请求，基本都是放阻塞队列</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">processRequest</span><span class="hljs-params">(Request request)</span> </span>&#123;
        <span class="hljs-keyword">if</span> (!finished) &#123;
            queuedRequests.add(request);
        &#125;
    &#125;
&#125;</code></pre></div>
<br>
<p>从源码就可以看出来，所有的修改请求，都会被存入<strong>阻塞队列</strong>。</p>
<p>然后<strong>单线程</strong>慢慢的一个个处理它们。</p>
<p>处理的过程就是将他们<strong>转发给leader节点</strong>。</p>
<p>对于读请求，线程本身就是安全的。这点不用担心。</p>
<h2 id="CAP理论🛰"><a class="header-anchor" href="#CAP理论🛰"></a>CAP理论🛰</h2>
<h3 id="1）什么是CAP？"><a class="header-anchor" href="#1）什么是CAP？"></a>1）什么是CAP？</h3>
<p><strong>CAP</strong>不是什么专业名词，它是由三个名词的首字母拼起来的：</p>
<p><strong>1.Consistent 一致性</strong></p>
<p><strong>2.Availability 可用性</strong></p>
<p><strong>3.Partition tolerance 分区容忍性</strong></p>
<br>
<p>CAP理论可以认为是分布式领域的牛顿定律，基石。</p>
<h3 id="2）CAP核心思想"><a class="header-anchor" href="#2）CAP核心思想"></a>2）CAP核心思想</h3>
<p>看起来CAP是连在一起的，但是在实际的分布式场景下，它们是分开的！</p>
<br>
<p><strong>网络分区的产生：</strong></p>
<p>我们先从P开始说起，在分布式场景下，各个服务器之间是需要进行通信的。</p>
<p>可能是同一机房下的，也有可能跨机房的。</p>
<p>不管怎么说，网络总是会出意外的！可能某个时刻网络就断开了。</p>
<p>那么集群之间的通信势必就会受到影响！这个时候就产生了<strong>网络分区</strong>。</p>
<br>
<p>当两台机器A和B之间无法进行通信时，那么有可能A服务器接受到了新请求，并对数据进行了更新。</p>
<p>此时的B无法感知到数据的更新（无法通信）。</p>
<p>如果什么都不做的话，当过一段时间网络恢复了。</p>
<p>B中的数据就会落后于A！这个时候就会造成两者之间的数据不一致！</p>
<br>
<p><strong>牺牲高可用</strong></p>
<p>假设你的线上服务对高可用性没有什么要求，但是非常注重数据的一致性。</p>
<p>那么这个时候你就可以考虑暂停一段时间的线上服务，牺牲可用性。马上对故障进行恢复。</p>
<p>等到故障恢复了，在重新开启A和B的服务。</p>
<br>
<p><strong>牺牲一致性</strong></p>
<p>但是如果你的业务不要求数据是强一致性的，但是要求线上服务是高可用的。</p>
<p>也就是说你的服务能够容忍一段时间的数据不一致，但是必须要求服务可以立马做出响应。</p>
<p>这个时候你就需要考虑不停止线上服务，故障恢复的同时继续为线上提供服务。</p>
<br>
<p>可以看到当网络分区发生时，<strong>一致性</strong>C和<strong>可用性</strong>A只能保一个！</p>
<p>换句话来说，实际分布式的场景下，你最多做到CP或者AP，但是你永远无法做到CAP共存的情况！</p>
<p><strong>这就是CAP的理论。</strong></p>
<h3 id="3）ZK的模式是什么？"><a class="header-anchor" href="#3）ZK的模式是什么？"></a>3）ZK的模式是什么？</h3>
<p>很显然，我们知道ZK本身对集群有着很好的支持。</p>
<p>而且ZK中的ZAB协议本身就是对集群之间的数据一致性做了很多努力。</p>
<br>
<p>但是ZK本身也有很多的问题。</p>
<p><strong>问题1：选举过程中的不可用</strong></p>
<p>比如说在发生Leader节点挂掉了，此时马上就会就进行选举。</p>
<p>但是在选举的过程很复杂，耗时也很长。</p>
<p>而在选举新的Leader的过程中，线上是无法提供请求的。</p>
<p>相当于牺牲了可用性A</p>
<p>所以本质上ZK是一个CP</p>
<p>这里需要提到的是，在新版的ZK中，多了一种叫做Observer的角色。</p>
<p>这个角色不会直接参与Leader的选举过程。</p>
<p>所以在选Leader的过程中，Observer可以作为备用的节点对线上服务提供读的服务。</p>
<p>但是如果此时需要写操作，服务依旧是不可用的。</p>
<p>其实对于这种问题，其实还有一种解决方案就是设置本地缓存，当ZK不可用的时候就先用缓存顶一顶。</p>
<br>
<p><strong>问题2：分区下的不可用</strong></p>
<p>不仅如此，如果是在分机房部署的场景下，如下图所示：</p>
<p><img src="https://bins-pic.oss-cn-shanghai.aliyuncs.com/mypic/ZK%E7%9A%84%E9%97%AE%E9%A2%98%E4%B9%8B%E7%BD%91%E7%BB%9C%E5%88%86%E5%8C%BA%E4%B8%8B%E7%9A%84ZK.jpg" srcset="/binblog/img/loading.gif" alt=""></p>
<p>如果此时的机房3和机房1和2之间断开了网络连接。</p>
<p>那么处理机房3中的服务虽然无法和外面进行通信，但是本地至少还有一个ZK节点！</p>
<p>有总比没有好！处于机房3中的服务就会去找处于本地机房的ZK节点。</p>
<p>但是假设此时的Leader就在机房3中。</p>
<p>现在一旦其他节点无法连接到Leader节点，就会触发选举事件。</p>
<p>但是选举还是依赖于节点的个数（<strong>2个以上</strong>）。</p>
<p>假设在机房3中，只有一台节点，此时处于机房3中的ZK集群直接不可用。</p>
<p>这里的不可用同时指的是不能写，不能读！</p>
<p>那么此时处于机房3中的服务也就变得不可用的！</p>
<br>
<p><strong>问题3：ZK写的性能问题</strong></p>
<p>我们知道，在ZK集群中，只有Leader节点才能处理写操作，其他从节点收到写操作都会自动转发给Leader节点。</p>
<p>但是如果在高并发的写请求下，Leader只有一个，压力巨大。性能自然就不用说了。</p>
<p>​</p>

            </article>
            <hr>
            <div>
              <div class="post-metas mb-3">
                
                  <div class="post-meta mr-3">
                    <i class="iconfont icon-category"></i>
                    
                      <a class="hover-with-bg" href="/binblog/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/">中间件</a>
                    
                  </div>
                
                
              </div>
              
                <p class="note note-warning">本博客所有文章除特别声明外，均采用 <a href="https://creativecommons.org/licenses/by-sa/4.0/deed.zh" target="_blank" rel="nofollow noopener noopener">CC BY-SA 4.0 协议</a> ，转载请注明出处！</p>
              
              
                <div class="post-prevnext row">
                  <div class="post-prev col-6">
                    
                    
                      <a href="/binblog/2020/09/30/%E7%A7%8B%E6%8B%9B%E6%9D%82%E8%B0%88/">
                        <i class="iconfont icon-arrowleft"></i>
                        <span class="hidden-mobile">秋招杂谈</span>
                        <span class="visible-mobile">上一篇</span>
                      </a>
                    
                  </div>
                  <div class="post-next col-6">
                    
                    
                      <a href="/binblog/2020/08/02/Typora%E4%B8%ADEmoji%E9%9B%86%E5%90%88/">
                        <span class="hidden-mobile">Typora中Emoji指令集合</span>
                        <span class="visible-mobile">下一篇</span>
                        <i class="iconfont icon-arrowright"></i>
                      </a>
                    
                  </div>
                </div>
              
            </div>

            
              <!-- Comments -->
              <div class="comments" id="comments">
                
                
  <div id="vcomments"></div>
  <script defer src="https://cdn.staticfile.org/valine/1.4.14/Valine.min.js" ></script>

  <script type="text/javascript">
    var oldLoadVa = window.onload;
    window.onload = function () {
      oldLoadVa && oldLoadVa();

      new Valine({
        el: "#vcomments",
        app_id: "vsanVmkaurLHyq2Xey9odo5V-gzGzoHsz",
        app_key: "v2hMMe4Mzz3mqpy9jwd7qx8B",
        placeholder: "说点什么吧！",
        path: window.location.pathname,
        avatar: "retro",
        meta: ["nick","mail","link"],
        pageSize: "10",
        lang: "zh-CN",
        highlight: true,
        recordIP: false,
        serverURLs: "",
      });
    };
  </script>
  <noscript>Please enable JavaScript to view the <a href="https://valine.js.org" target="_blank" rel="nofollow noopener noopener">comments
      powered by Valine.</a></noscript>


              </div>
            
          </div>
        </div>
      </div>
    </div>
    
      <div class="d-none d-lg-block col-lg-2 toc-container" id="toc-ctn">
        <div id="toc">
  <p class="toc-header"><i class="iconfont icon-list"></i>&nbsp;目录</p>
  <div id="tocbot"></div>
</div>

      </div>
    
  </div>
</div>

<!-- Custom -->


    
  </main>

  
    <a id="scroll-top-button" href="#" role="button">
      <i class="iconfont icon-arrowup" aria-hidden="true"></i>
    </a>
  

  
    <div class="modal fade" id="modalSearch" tabindex="-1" role="dialog" aria-labelledby="ModalLabel"
     aria-hidden="true">
  <div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
    <div class="modal-content">
      <div class="modal-header text-center">
        <h4 class="modal-title w-100 font-weight-bold">搜索</h4>
        <button type="button" id="local-search-close" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body mx-3">
        <div class="md-form mb-5">
          <input type="text" id="local-search-input" class="form-control validate">
          <label data-error="x" data-success="v"
                 for="local-search-input">关键词</label>
        </div>
        <div class="list-group" id="local-search-result"></div>
      </div>
    </div>
  </div>
</div>
  

  

  

  <footer class="mt-5">
  <div class="text-center py-3">
    <div>
      <a href="https://hexo.io" target="_blank" rel="nofollow noopener"><span>Hexo</span></a>
      <i class="iconfont icon-love"></i>
      <a href="https://github.com/fluid-dev/hexo-theme-fluid" target="_blank" rel="nofollow noopener">
        <span>Fluid</span></a>
    </div>
    
  <div class="statistics">
    
    

    
      
        <!-- LeanCloud 统计PV -->
        <span id="leancloud-site-pv-container" style="display: none">
            总访问量 
            <span id="leancloud-site-pv"></span>
             次
          </span>
      
      
        <!-- LeanCloud 统计UV -->
        <span id="leancloud-site-uv-container" style="display: none">
            总访客数 
            <span id="leancloud-site-uv"></span>
             人
          </span>
      

    
  </div>


    
  <!-- 备案信息 -->
  <div class="beian">
    <a href="http://beian.miit.gov.cn/" target="_blank"
       rel="nofollow noopener">京ICP证123456号</a>
    
      <a
        href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=12345678"
        rel="nofollow noopener"
        class="beian-police"
        target="_blank"
      >
        <span class="beian-police-sep">&nbsp;|&nbsp;</span>
        
          <img src="/binblog/img/police_beian.png" srcset="/binblog/img/loading.gif" alt="police-icon" />
        
        <span>京公网安备12345678号</span>
      </a>
     
  </div>


    
  </div>
</footer>

<!-- SCRIPTS -->
<script  src="https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js" ></script>
<script  src="https://cdn.staticfile.org/twitter-bootstrap/4.4.1/js/bootstrap.min.js" ></script>
<script  src="/binblog/js/debouncer.js" ></script>
<script  src="/binblog/js/main.js" ></script>

<!-- Plugins -->


  
    <script  src="/binblog/js/lazyload.js" ></script>
  



  <script defer src="https://cdn.staticfile.org/clipboard.js/2.0.6/clipboard.min.js" ></script>
  <script  src="/binblog/js/clipboard-use.js" ></script>



  <script defer>
  (function () {
    // 查询存储的记录
    function getRecord(Counter, target) {
      return new Promise(function (resolve, reject) {
        Counter('get', '/classes/Counter?where=' + encodeURIComponent(JSON.stringify({target})))
          .then(resp => resp.json())
          .then(({results, code, error}) => {
            if (code === 401) {
              throw error;
            }
            if (results && results.length > 0) {
              var record = results[0];
              resolve(record);
            } else {
              Counter('post', '/classes/Counter', {target, time: 0})
                .then(resp => resp.json())
                .then((record, error) => {
                  if (error) {
                    throw error;
                  }
                  resolve(record);
                }).catch(error => {
                console.error('Failed to create', error);
                reject(error);
              });
            }
          }).catch((error) => {
          console.error('LeanCloud Counter Error:', error);
          reject(error);
        });
      })
    }

    // 发起自增请求
    function increment(Counter, incrArr) {
      return new Promise(function (resolve, reject) {
        Counter('post', '/batch', {
          "requests": incrArr
        }).then((res) => {
          res = res.json();
          if (res.error) {
            throw res.error;
          }
          resolve(res);
        }).catch((error) => {
          console.error('Failed to save visitor count', error);
          reject(error);
        });
      });
    }

    // 构建自增请求体
    function buildIncrement(objectId) {
      return {
        "method": "PUT",
        "path": `/1.1/classes/Counter/${ objectId }`,
        "body": {
          "time": {
            '__op': 'Increment',
            'amount': 1
          }
        }
      }
    }

    // 校验是否为有效的 UV
    function validUV() {
      var key = 'LeanCloud_UV_Flag';
      var flag = localStorage.getItem(key);
      if (flag) {
        // 距离标记小于 24 小时则不计为 UV
        if (new Date().getTime() - parseInt(flag) <= 86400000) {
          return false;
        }
      }
      localStorage.setItem(key, new Date().getTime().toString());
      return true;
    }

    function addCount(Counter) {
      var enableIncr = 'true' === 'true' && window.location.hostname !== 'localhost';
      var getterArr = [];
      var incrArr = [];

      // 请求 PV 并自增
      var pvCtn = document.querySelector('#leancloud-site-pv-container');
      if (pvCtn || enableIncr) {
        var pvGetter = getRecord(Counter, 'site-pv').then((record) => {
          incrArr.push(buildIncrement(record.objectId))
          var ele = document.querySelector('#leancloud-site-pv');
          if (ele) {
            ele.innerText = record.time + 1;
            if (pvCtn) {
              pvCtn.style.display = 'inline';
            }
          }
        });
        getterArr.push(pvGetter);
      }

      // 请求 UV 并自增
      var uvCtn = document.querySelector('#leancloud-site-uv-container');
      if (uvCtn || enableIncr) {
        var uvGetter = getRecord(Counter, 'site-uv').then((record) => {
          var vuv = validUV();
          vuv && incrArr.push(buildIncrement(record.objectId))
          var ele = document.querySelector('#leancloud-site-uv');
          if (ele) {
            ele.innerText = record.time + (vuv ? 1 : 0);
            if (uvCtn) {
              uvCtn.style.display = 'inline';
            }
          }
        });
        getterArr.push(uvGetter);
      }

      // 如果是文章，请求文章的浏览数，并自增
      if ('true' === 'true') {
        var viewCtn = document.querySelector('#leancloud-post-views-container');
        if (viewCtn || enableIncr) {
          var target = decodeURI('/binblog/2020/08/09/ZooKeeper%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA/');
          var viewGetter = getRecord(Counter, target).then((record) => {
            incrArr.push(buildIncrement(record.objectId))
            if (viewCtn) {
              var ele = document.querySelector('#leancloud-post-views');
              if (ele) {
                ele.innerText = (record.time || 0) + 1;
                viewCtn.style.display = 'inline';
              }
            }
          });
          getterArr.push(viewGetter);
        }
      }

      // 如果启动计数自增，批量发起自增请求
      if (enableIncr) {
        Promise.all(getterArr).then(() => {
          incrArr.length > 0 && increment(Counter, incrArr);
        })
      }
    }

    var app_id = 'vsanVmkaurLHyq2Xey9odo5V-gzGzoHsz'
    var app_key = 'v2hMMe4Mzz3mqpy9jwd7qx8B'
    var server_url = ''

    function fetchData(api_server) {
      var Counter = (method, url, data) => {
        return fetch(`${ api_server }/1.1${ url }`, {
          method,
          headers: {
            'X-LC-Id': app_id,
            'X-LC-Key': app_key,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(data)
        });
      };

      addCount(Counter);
    }

    var api_server = app_id.slice(-9) !== '-MdYXbMMI' ? server_url : `https://${ app_id.slice(0, 8).toLowerCase() }.api.lncldglobal.com`;

    if (api_server) {
      fetchData(api_server);
    } else {
      fetch('https://app-router.leancloud.cn/2/route?appId=' + app_id)
        .then(resp => resp.json())
        .then(({api_server}) => {
          fetchData('https://' + api_server);
        });
    }
  })();
</script>






  <script  src="https://cdn.staticfile.org/tocbot/4.11.1/tocbot.min.js" ></script>
  <script>
    $(document).ready(function () {
      var boardCtn = $('#board-ctn');
      var boardTop = boardCtn.offset().top;

      tocbot.init({
        tocSelector: '#tocbot',
        contentSelector: 'article.markdown-body',
        headingSelector: 'h1,h2,h3,h4,h5,h6',
        linkClass: 'tocbot-link',
        activeLinkClass: 'tocbot-active-link',
        listClass: 'tocbot-list',
        isCollapsedClass: 'tocbot-is-collapsed',
        collapsibleClass: 'tocbot-is-collapsible',
        collapseDepth: 0,
        scrollSmooth: true,
        headingsOffset: -boardTop
      });
      if ($('.toc-list-item').length > 0) {
        $('#toc').css('visibility', 'visible');
      }
    });
  </script>



  <script  src="https://cdn.staticfile.org/typed.js/2.0.11/typed.min.js" ></script>
  <script>
    var typed = new Typed('#subtitle', {
      strings: [
        '  ',
        "ZooKeeper深入浅出&nbsp;",
      ],
      cursorChar: "_",
      typeSpeed: 70,
      loop: false,
    });
    typed.stop();
    $(document).ready(function () {
      $(".typed-cursor").addClass("h2");
      typed.start();
    });
  </script>



  <script  src="https://cdn.staticfile.org/anchor-js/4.2.2/anchor.min.js" ></script>
  <script>
    anchors.options = {
      placement: "right",
      visible: "hover",
      
    };
    var el = "h1,h2,h3,h4,h5,h6".split(",");
    var res = [];
    for (item of el) {
      res.push(".markdown-body > " + item)
    }
    anchors.add(res.join(", "))
  </script>



  <script  src="/binblog/js/local-search.js" ></script>
  <script>
    var path = "/binblog/local-search.xml";
    var inputArea = document.querySelector("#local-search-input");
    inputArea.onclick = function () {
      searchFunc(path, 'local-search-input', 'local-search-result');
      this.onclick = null
    }
  </script>



  <script  src="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.js" ></script>
  <link  rel="stylesheet" href="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.css" />

  <script>
    $('#post img:not(.no-zoom img, img[no-zoom]), img[zoom]').each(
      function () {
        var element = document.createElement('a');
        $(element).attr('data-fancybox', 'images');
        $(element).attr('href', $(this).attr('src'));
        $(this).wrap(element);
      }
    );
  </script>





  

  
    <!-- MathJax -->
    <script>
      MathJax = {
        tex: {
          inlineMath: [['$', '$'], ['\\(', '\\)']]
        },
        options: {
          renderActions: {
            findScript: [10, doc => {
              document.querySelectorAll('script[type^="math/tex"]').forEach(node => {
                const display = !!node.type.match(/; *mode=display/);
                const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display);
                const text = document.createTextNode('');
                node.parentNode.replaceChild(text, node);
                math.start = { node: text, delim: '', n: 0 };
                math.end = { node: text, delim: '', n: 0 };
                doc.math.push(math);
              });
            }, '', false],
            insertedScript: [200, () => {
              document.querySelectorAll('mjx-container').forEach(node => {
                let target = node.parentNode;
                if (target.nodeName.toLowerCase() === 'li') {
                  target.parentNode.classList.add('has-jax');
                }
              });
            }, '', false]
          }
        }
      };
    </script>

    <script async src="https://cdn.staticfile.org/mathjax/3.0.5/es5/tex-svg.js" ></script>

  













  

  

  

  

  

  





<script src="/binblog/live2dw/lib/L2Dwidget.min.js?094cbace49a39548bed64abff5988b05"></script><script>L2Dwidget.init({"model":{"jsonPath":"/binblog/live2dw/assets/koharu.model.json"},"display":{"position":"right","width":150,"height":300},"mobile":{"show":true},"log":false,"pluginJsPath":"lib/","pluginModelPath":"assets/","pluginRootPath":"live2dw/","tagMode":false});</script></body>
</html>
